package com.google.code.expr.lang;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;

/**
 * Primary parser of the EXPR protocol.  Parses live on a stream.
 * It will read the next "atom", which is either a raw text stream
 * or a single EXPR command, and returns it.
 * 
 * Usage:
 * EXPRAtomReader reader = new EXPRReader(new FileReader("file.txt"));
 * EXPRAtom atom = reader.read();
 * while (atom!=null) {
 *   atom = reader.read();
 * }
 * 
 * @author critchie
 *
 */
public class EXPRAtomReader {

	private static final char START_COMMAND_1 = '{';
	private static final char START_COMMAND_2 = '{';
	private static final char END_COMMAND_1 = '}';
	private static final char END_COMMAND_2 = '}';

	private boolean mTrimStream;
	private BufferedReader reader;
	
	private int mLineNumber=1; //current line number within file
	private int mColumn = 1; //current column if the current line number
	private boolean mIsParsingCommand = false; //true while parsing EXPR commands between {{ }}
	private int mStartLineNumberOfAtom = 1; //starting line number of the current parse run()
	private int mStartColumnOfAtom = 1; //starting column of the starting line number of the current parse run()
	private String mCurrentLine = null; //the latest line read from the stream
	
	/**
	 * Constructs the reader with the inputsteam that will
	 * be parsed.
	 * 
	 * @param pIS
	 * @throws EXPRParseException
	 */
	public EXPRAtomReader(InputStream pIS) throws EXPRParseException {
		this(new InputStreamReader(pIS));
	}
	/**
	 * Constructs the reader with the inputsteam that will
	 * be parsed.
	 * 
	 * @param pIS
	 * @throws EXPRParseException
	 */
	public EXPRAtomReader(Reader pReader) throws EXPRParseException {
		reader = new BufferedReader(pReader);
		try {
			mCurrentLine = reader.readLine();
		}
		catch (IOException e) {
			throw new EXPRParseException(mLineNumber, mColumn, "Error Parsing File: "+e.getMessage(),e);
		}
	}
	
	/**
	 * closes the internal reader.
	 * 
	 * @throws IOException
	 */
	public void close() throws IOException {
		reader.close();
	}
	/**
	 * If true, it will trim whitespace around a text stream
	 */
	public boolean isTrimStream() {
		return mTrimStream;
	}

	/**
	 * If true, it will trim whitespace around a text stream
	 */
	public void setTrimStream(boolean mTrimStream) {
		this.mTrimStream = mTrimStream;
	}

	/**
	 * <p>The primary method of this reader.  call this method
	 * to get the next atom from the source stream.  The atom
	 * will either be a parsed command, or a full block of raw
	 * text.</p>
	 * <p>This reader does not know if it is within a child
	 * block or not - it simply reads the next atom.</p>
	 * 
	 * @return the next atom from the stream, or null if it is done.
	 * @throws EXPRParseException
	 */
	public EXPRAtom read() throws EXPRParseException {
		try {
			StringWriter writer = new StringWriter();
			PrintWriter out = new PrintWriter(writer);
			while (mCurrentLine!=null) {
				for (; mColumn<=mCurrentLine.length(); mColumn++) {
					char c = mCurrentLine.charAt(mColumn-1);
					
					if (mIsParsingCommand) {
						if (c==END_COMMAND_1 && mColumn<(mCurrentLine.length()) && mCurrentLine.charAt(mColumn)==END_COMMAND_2) {
							//end if command
							out.flush();
							writer.flush();
							EXPRAtom command = createCommand(writer.toString());
							
							
							mColumn+=2;
							mStartLineNumberOfAtom = mLineNumber;
							mStartColumnOfAtom = mColumn;
							mIsParsingCommand = false;
							return command;
						}
						else {
							out.print(c);
						}
					}
					else {
						if (c==START_COMMAND_1 && mColumn<(mCurrentLine.length()-1) && mCurrentLine.charAt(mColumn)==START_COMMAND_2) {
							//start of command
							out.flush();
							writer.flush();
							EXPRStream stream = createStream(writer.toString());
							mStartLineNumberOfAtom = mLineNumber;
							mStartColumnOfAtom = mColumn;
							mIsParsingCommand = true;
							if (stream!=null) {
								mColumn+=2;
								return stream;
							}
							else {
								mColumn+=1;
								writer = new StringWriter();
								out = new PrintWriter(writer);
							}
						}
						else {
							out.print(c);
						}
					}
				}
				
				mCurrentLine = reader.readLine();
				if (mCurrentLine!=null) {
					out.println();
				}
				mLineNumber++;
				mColumn=1;
			}
			out.flush();
			writer.flush();
			if (writer.getBuffer().length()>0) {
				if (mIsParsingCommand) {
					throw new EXPRParseException(mLineNumber, mColumn, "Still in command at the end of the file");
				}
				else {
					EXPRStream stream = createStream(writer.toString());
					if (stream!=null) {
						return stream;
					}
				}
			}
			//end of file
			return null;
		}
		catch (EXPRParseException e) {
			throw e;
		}
		catch (IOException e) {
			throw new EXPRParseException(mLineNumber, mColumn, "Error Parsing File: "+e.getMessage(),e);
		}
	}

	/**
	 * Called by the read() method when it completes reading of a raw text stream, it creates
	 * the stream atom and returns it.
	 * 
	 * @param pString the raw text
	 * @return the created atom, or null if the text was all whitespace
	 */
	protected EXPRStream createStream(String pString) {
		EXPRStream stream = null;
	
		if (pString!=null) {
			String trimmed = trimString(pString);
			if (trimmed.length()>0) {
				stream = new EXPRStream(isTrimStream() ? trimmed : pString);
				stream.setScriptLocation(mStartLineNumberOfAtom, mStartColumnOfAtom);
			}
		}
		return stream;
	}
	
	/**
	 * Takes the content of a command and parses it into an atom.
	 * It uses the javacc created EXPRXCommandParser to do the
	 * parsing.
	 * 
	 * @param pStream the raw text of the command
	 * @return a created atom
	 * @throws EXPRParseException
	 */
	protected EXPRAtom createCommand(String pStream) throws EXPRParseException {
		EXPRCommandParser commandParser = new EXPRCommandParser(pStream);
		commandParser.setLineNumberOffset(mStartLineNumberOfAtom);
		commandParser.setColumnOffset(mStartColumnOfAtom);
		commandParser.parse();
		return commandParser.getCommand();
	}

	/**
	 * Trims both sides of a string.
	 * @param pString
	 * @return
	 */
	protected String trimString(String pString) {
		if (pString==null) {
			return null;
		}
		char[] chars = pString.toCharArray();
		int left=0;
		int right=pString.length()-1;
		while (left<pString.length() && Character.isWhitespace(chars[left])) {
			left++;
		}
		while (right>=left && Character.isWhitespace(chars[right])) {
			right--;
		}
		if (left>right) {
			return "";
		}
		return pString.substring(left,right+1);
	}
	
}
